最近一直聽到公司同事再說 Go 的高流量高併發,那這兩個又是甚麼呢?
這邊舉一個生活中的例子這樣大家比較好懂:
假設你是一家快遞公司的管理員,而快遞員則是 Goroutine
。你收到了一大堆客戶的包裹送遞訂單(代表高流量),而你的任務是盡快將這些包裹送遞到目的地。
這裡有兩種方式可以處理這個任務:
Goroutine
)。每個快遞員獨立地處理一個包裹的送遞請求,而不需要等待其他快遞員完成任務。Goroutine
實現的並發程式設計,每個 Goroutine
可以獨立地執行任務,並且可以同時進行多個任務,從而提高了效率。Channel
則扮演著快遞公司的郵件系統的角色。每個快遞員(Goroutine
)在完成任務後,都會將包裹的狀態信息通過郵件系統(Channel
)發送給你。你可以隨時查看這些郵件,瞭解每個包裹的狀態,從而及時跟進。通過使用 Goroutine
和 Channel
,你可以像管理一家快遞公司一樣高效地處理大量的任務和資料,並確保任務的及時完成和準確交付。
在 Go 的併發機制運作十分簡便,只要偷過 go 關鍵字就可以開啟 goroutine 了。
Goroutine
可以使用匿名函式來宣告,也可以使用具名函式來宣告,宣告的方式如下:
func sayHello() {
time.Sleep(600 * time.Millisecond)
fmt.Println("Hello from named Goroutine!")
}
func main() {
// Anonymous Goroutine
go func() {
time.Sleep(500 * time.Millisecond)
fmt.Println("Hello from anonymous Goroutine!")
}()
// Named Goroutine
go sayHello()
time.Sleep(3 * time.Second)
fmt.Println("Main function exiting...")
}
上面程式碼中,有兩個 Goroutine,一個匿名另外一個為具名,接下來,舉個簡單的例子來演示一下 Goroutine 的運作方式,首先,會有四個任務,有三個需要花點時間去執行,另外一個一下子就可以得到結果了:
package main
import (
"fmt"
"time"
)
func taskA() {
for i := 1; i <= 5; i++ {
fmt.Println("Task A:", i)
time.Sleep(200 * time.Millisecond)
}
}
func taskB() {
for i := 1; i <= 5; i++ {
fmt.Println("Task B:", i)
time.Sleep(300 * time.Millisecond)
}
}
func taskC() {
for i := 1; i <= 5; i++ {
fmt.Println("Task C:", i)
time.Sleep(400 * time.Millisecond)
}
}
func normalTask() {
fmt.Println("Normal Task: Running")
time.Sleep(500 * time.Millisecond)
fmt.Println("Normal Task: Completed")
}
func main() {
go taskA()
go taskB()
go taskC()
normalTask()
time.Sleep(3 * time.Second)
}
執行結果:
Normal Task: Running
Task C: 1
Task B: 1
Task A: 1
Task A: 2
Task B: 2
Task A: 3
Task C: 2
Normal Task: Completed
Task A: 4
Task B: 3
Task A: 5
Task C: 3
Task B: 4
Task C: 4
Task B: 5
Task C: 5
你會不會覺得好像哪裡怪怪的,照邏輯來看 Task A
應該是要先印出東西出來,為甚麼是 Task C
先印出來?
這是由於 Goroutine
是非阻塞的,所以它們的執行順序是不確定的,這取決於 Goroutine
被調度的順序、CPU
資源的分配以及休眠時間的長短等因素。
但這就衍伸出一個問題,如果要讓 main()
函式等待 Goroutine
執行結束後才退出,我們需要一種機制來知道何時 Goroutine
應該退出,以及如何知道所有 Goroutine
都已經結束。這通常通過使用通道channel
來實現,通道可用於 Goroutine
之間的溝通和同步。
通道 channel
是一個用來在傳遞資料的資料結構。當一個資源需要在 goroutine
之間共用時,它便在 goroutine
之間架起了一個通道,並確保同步交換資料的機制,讓 goroutine
之間可以安全地傳遞數據。
而 Channel
是一種特別的類型。在任何時候,同時只能有一個 goroutine 存取通道進行發送和接受資料。使用這種佇列的方式是最為高效的,它遵循著先入先出 (First In First Out) 的規則,從而保證了收發資料的順序。
那我們要怎麼創建 channel
呢?可以使用 make()
函式來創建。通道的類型是使用 chan
加上元素的類型表示的,如下:
// 創建一個整數類型的通道
ch1 := make(chan int)
// 創建一個字符串類型的通道
ch2 := make(chan string)
// 創建一個自定義類型的通道
type CustomType struct {
// 自定義類型的字段
}
ch3 := make(chan CustomType)
當 Channel 倍創建完後,就可以利用它來收發資料了,那要怎麼做呢?
Channel 發送資料的格式長這樣:
通道變數 <- 通道值
這裡稍微解釋一下,通道變數就是剛剛使用 make() 創建好的實例,通道值可以是變數、字串、函數返回值或運算式等,只要跟剛 Channel 類型一致就好了。
那我們就來發資料吧!
func main() {
ch := make(chan string)
ch <- "data"
}
當我們執行後,發現出現 fatal error: all goroutines are asleep - deadlock!
,為什麼會這樣呢?
這是因為當把資料往 channel 中發送時,如果接收方一直沒有接受,發送的操作就會持續阻塞。 Go 則可以在執行期間發現一些永遠無法發送成功的地方並作出提示。也就會出現剛剛上面的提示。
既然都已經發送資料過來了,那就接收吧!要怎麼接收呢?一樣也是用 <-,用 Channel 接收資料也幾個特性:
接收的方式有四種:
阻塞接收資料:
當使用 <- 來接收單個資料元素時,如果通道中沒有可用的資料,接收操作將會阻塞,直到有資料可供接收為止。這種情況下,接收操作會一直等待,直到資料被發送到通道中為止。
data := <-ch
執行該段程式將阻塞,直到接收到資料並傳給 data。
非阻塞接收資料:
使用這種非阻塞的寫法從 Channel 接收資料時,將不會發生阻塞。
data, ok := <-ch
data 為接收到的資料, 如未接收到資料時,data 為 Channel 類型的零值。ok 為是否接收到資料。
但這種接收方式,可能會造成 CPU 的高佔用,因此使用的很少。
接收任意資料,忽略掉接收的值:
下面這種寫法,接收到的值將會被忽略:
<-ch
執行該程式會發生阻塞,直到接收到資料為止,但接收到的資料會被忽略掉。這個方法實際上只是透過 goroutine
間阻賽收發,進而實現併發同步,實際的作法如下:
func main() {
ch := make(chan string)
go func () {
fmt.Println("開始 goroutine")
ch <- "signal"
fmt.Println("退出 goroutine")
}()
fmt.Println("等待 goroutine")
<-ch
fmt.Println("完成")
}
執行結果:
等待 goroutine
開始 goroutine
退出 goroutine
完成
主程式在 <-ch
這一行會進行通道的接收操作,所以如果在這之前通道 ch
中還沒有資料,主程式會被阻塞,直到從 goroutine
中的 ch <- "signal"
行發送了資料到通道中為止。這樣就確保了主程式會等待 goroutine 中的資料傳入後才會繼續執行下一步。
使用迴圈接收資料:
如果需要進行多個元素的接收操作,那我們可以透過 for-range
來進行資料的接收:
for data := range ch {
}
Channel 是可以被遍歷的,遍歷的結果就是接收到的資料。透過 for 遍歷獲得的變數只有一個,就是上面程式碼的 data,實際操作如下:
/*
這段程式碼,有一個 `goroutine` 在通道 ch 中發送整數資料,而主程式在通道 ch 上進行接收。接收到通道中的資料後,主程式會將資料印出來,並檢查是否有 6 的元素,如果有,則印出 "通道中有 6 的元素" 並結束程式。
*/
func main() {
ch := make(chan int)
go func(){
fmt.Println("開始 goroutine")
for i := 1; i <= 8; i++{
ch <- i
time.Sleep(time.Second)
}
fmt.Println("退出 goroutine")
}()
fmt.Println("等待 goroutine")
for receive := range ch {
fmt.Println(receive)
if receive == 6 {
fmt.Println("通道中有 6 的元素")
time.Sleep(time.Second)
break
}
}
}
執行結果:
等待 goroutine
開始 goroutine
3
4
5
6
通道中有 6 的元素
主程式啟動了一個新的 goroutine
,這個 goroutine
中執行了一個匿名函式。在這個匿名函式中,使用了一個迴圈從 3 到 8 發送整數到通道 ch 中,每次發送完後休眠一秒鐘,然後輸出一條訊息 "退出 goroutine"。
主程式輸出一條訊息 "等待 goroutine",然後進入一個無窮迴圈,用來持續從通道 ch 中接收資料。當接收到資料後,將其印出來,然後檢查是否為 6,如果是,則輸出 "通道中有 6 的元素",然後休眠一秒鐘並跳出迴圈。
當 goroutine
中的迴圈發送完所有資料後,如果主程式還在等待接收通道中的資料,主程式會持續等待。當接收到 6 之後,主程式輸出 "通道中有 6 的元素",並在一秒後結束程式。
這個例子展示了 goroutine 的併發特性,以及通道收發資料的特性。透過這種方式,可以在不同的 goroutine 中進行併發處理,並通過通道進行資料交換,實現程式的併發執行。
以上就是在 Go 中高流量高併發的介紹,當然還不只這些,還有很多可以來解決這個問題,還請大家慢曼去發掘吧!那我們明天見!
參考資料: